@fieldwangai/agentflow 0.1.69 → 0.1.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/lib/apply.mjs CHANGED
@@ -24,6 +24,7 @@ import { printEntryAndFlowFiles, printNodeStatusTable, runValidateFlowAndExitIfI
24
24
  import { clearApplyActiveLock, writeApplyActiveLock } from "./run-apply-active-lock.mjs";
25
25
  import { ensureReference, findFlowNameByUuid, getFlowDir, getRunDir } from "./workspace.mjs";
26
26
  import { readMergedEnvObject } from "./user-env.mjs";
27
+ import { appendRunLedgerEvent } from "./run-ledger.mjs";
27
28
 
28
29
  const PARALLEL_PREFIX_COLORS = [
29
30
  (s) => chalk.cyan(s),
@@ -72,6 +73,31 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
72
73
  // 提前 ensure:apply-start 携带原始 run 起始时间,UI 计时器可按「这个 uuid 从头到现在的总时长」显示,
73
74
  // 避免每次 resume 仅从 totalExecutedMs 累加看起来像从 resume 才开始计时。
74
75
  const priorRunStartTime = ensureRunStartTime(workspaceRoot, flowName, uuid);
76
+ let runStartTime = priorRunStartTime;
77
+ let totalExecutedMs = priorTotalExecutedMs;
78
+ const writeRunLedger = !dryRun;
79
+ let runLedgerFinished = false;
80
+ const runLedgerBase = {
81
+ kind: "pipeline",
82
+ runId: uuid,
83
+ userId: String(process.env.AGENTFLOW_USER_ID || ""),
84
+ username: String(process.env.AGENTFLOW_USER_ID || ""),
85
+ flowId: flowName,
86
+ flowSource: "user",
87
+ at: priorRunStartTime,
88
+ };
89
+ if (writeRunLedger) appendRunLedgerEvent({ ...runLedgerBase, type: "run_started" });
90
+ const finishRunLedger = (status) => {
91
+ if (!writeRunLedger || runLedgerFinished) return;
92
+ runLedgerFinished = true;
93
+ appendRunLedgerEvent({
94
+ ...runLedgerBase,
95
+ type: "run_finished",
96
+ endedAt: Date.now(),
97
+ durationMs: totalExecutedMs,
98
+ status,
99
+ });
100
+ };
75
101
  emitEvent(workspaceRoot, flowName, uuid, {
76
102
  event: "apply-start",
77
103
  flowName,
@@ -85,8 +111,6 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
85
111
  writeApplyActiveLock(workspaceRoot, flowName, uuid);
86
112
 
87
113
  try {
88
- let runStartTime = priorRunStartTime;
89
- let totalExecutedMs = priorTotalExecutedMs;
90
114
  let round = 0;
91
115
  while (round < MAX_LOOP_ROUNDS) {
92
116
  round++;
@@ -100,6 +124,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
100
124
  if (readyNodes.length === 0) {
101
125
  if (allDone) {
102
126
  saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
127
+ finishRunLedger("success");
103
128
  const totalElapsed = formatDuration(totalExecutedMs);
104
129
  emitEvent(workspaceRoot, flowName, uuid, {
105
130
  event: "apply-done",
@@ -163,6 +188,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
163
188
  if (options.length === 0) {
164
189
  log.info(chalk.yellow(`节点 ${pendId} 未配置任何选项(output 槽位),无法选择。请先在编辑器中添加 output 槽位。`));
165
190
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
191
+ finishRunLedger("interrupted");
166
192
  return;
167
193
  }
168
194
 
@@ -177,6 +203,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
177
203
  }
178
204
  process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
179
205
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
206
+ finishRunLedger("interrupted");
180
207
  return;
181
208
  }
182
209
 
@@ -209,6 +236,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
209
236
  if (trimmed === "q") {
210
237
  rl.close();
211
238
  log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
239
+ finishRunLedger("interrupted");
212
240
  return;
213
241
  }
214
242
  const idx = parseInt(trimmed, 10);
@@ -250,6 +278,7 @@ export async function apply(workspaceRoot, flowName, uuidArg, dryRun, agentModel
250
278
  }
251
279
  process.stderr.write(chalk.bold.cyan("━━━━━━━━━━━━━━━━━━━━━━━━━\n"));
252
280
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
281
+ finishRunLedger("interrupted");
253
282
  return;
254
283
  }
255
284
 
@@ -380,6 +409,7 @@ ${currentContent}
380
409
  } else if (answer.trim().toLowerCase() === "q") {
381
410
  rl.close();
382
411
  log.info(chalk.yellow("用户取消,流程暂停。") + chalk.dim(` 恢复命令: ${resumeExample}`));
412
+ finishRunLedger("interrupted");
383
413
  return;
384
414
  }
385
415
  }
@@ -393,6 +423,7 @@ ${currentContent}
393
423
  }
394
424
 
395
425
  log.info(chalk.bold.yellow("→ " + t("flow.resume_hint") + " ") + resumeExample);
426
+ finishRunLedger("interrupted");
396
427
  return;
397
428
  }
398
429
  const endNodeIds = Array.isArray(parseOut.nodes)
@@ -401,6 +432,7 @@ ${currentContent}
401
432
  const endReached = endNodeIds.some((id) => instanceStatus[id] === "success");
402
433
  if (endReached) {
403
434
  saveTotalExecutedMs(workspaceRoot, flowName, uuid, totalExecutedMs);
435
+ finishRunLedger("success");
404
436
  const totalElapsed = formatDuration(totalExecutedMs);
405
437
  emitEvent(workspaceRoot, flowName, uuid, {
406
438
  event: "apply-done",
@@ -763,6 +795,7 @@ ${currentContent}
763
795
  maxErr.uuid = uuid;
764
796
  throw maxErr;
765
797
  } finally {
798
+ finishRunLedger("failed");
766
799
  clearApplyActiveLock(workspaceRoot, flowName, uuid);
767
800
  }
768
801
  }
@@ -158,6 +158,7 @@ function normalizeFrontmatterSlots(arr) {
158
158
  else if (typeof def !== "string") def = String(def);
159
159
  const slot = { type, name, default: def };
160
160
  if (item.required != null) slot.required = Boolean(item.required);
161
+ if (item.description != null) slot.description = String(item.description);
161
162
  slot.showOnNode = item.showOnNode != null ? Boolean(item.showOnNode) : defaultShowOnNodeForSlot(slot);
162
163
  return slot;
163
164
  });
@@ -289,6 +289,14 @@
289
289
  "displayName": "Create GitLab MR",
290
290
  "description": "Create or reuse a GitLab merge request for the current branch and output its URL"
291
291
  },
292
+ "tool_wecom_send_group_markdown": {
293
+ "displayName": "WeCom Group Markdown",
294
+ "description": "Send a Markdown message through a WeCom group robot webhook"
295
+ },
296
+ "tool_wecom_send_app_markdown": {
297
+ "displayName": "WeCom App Markdown",
298
+ "description": "Send a Markdown message to users through a WeCom enterprise application"
299
+ },
292
300
  "tool_nodejs": {
293
301
  "displayName": "Node.js Script",
294
302
  "description": "Execute Node.js script, success determined by exit code, stdout as result"
@@ -289,6 +289,14 @@
289
289
  "displayName": "提交 GitLab MR",
290
290
  "description": "为当前分支创建或复用 GitLab Merge Request,并输出 MR 链接"
291
291
  },
292
+ "tool_wecom_send_group_markdown": {
293
+ "displayName": "企业微信群 Markdown",
294
+ "description": "通过企业微信群机器人 webhook 发送 Markdown 消息"
295
+ },
296
+ "tool_wecom_send_app_markdown": {
297
+ "displayName": "企业微信应用 Markdown",
298
+ "description": "通过企业微信应用消息接口发送 Markdown 给用户"
299
+ },
292
300
  "tool_nodejs": {
293
301
  "displayName": "Node.js 脚本",
294
302
  "description": "执行 Node.js 脚本,以 exit code 判断成败,stdout 作为结果"
@@ -72,6 +72,7 @@ function normalizeSlotList(value) {
72
72
  default: def == null ? "" : String(def),
73
73
  };
74
74
  if (slot.required != null) normalized.required = Boolean(slot.required);
75
+ if (slot.description != null) normalized.description = String(slot.description);
75
76
  normalized.showOnNode = slot.showOnNode != null
76
77
  ? Boolean(slot.showOnNode)
77
78
  : Boolean(normalized.required) || type.toLowerCase() === "node";
package/bin/lib/paths.mjs CHANGED
@@ -302,6 +302,8 @@ export const LOCAL_ONLY_DEFINITION_IDS = new Set([
302
302
  "tool_git_worktree_load",
303
303
  "tool_git_worktree_unload",
304
304
  "tool_gitlab_create_mr",
305
+ "tool_wecom_send_group_markdown",
306
+ "tool_wecom_send_app_markdown",
305
307
  "tool_print",
306
308
  "tool_user_check",
307
309
  "tool_user_ask",
@@ -0,0 +1,96 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ import { getAgentflowDataRoot } from "./paths.mjs";
6
+
7
+ function localDayKey(timeMs) {
8
+ const d = new Date(Number(timeMs) || Date.now());
9
+ const y = d.getFullYear();
10
+ const m = String(d.getMonth() + 1).padStart(2, "0");
11
+ const day = String(d.getDate()).padStart(2, "0");
12
+ return `${y}-${m}-${day}`;
13
+ }
14
+
15
+ function safeSegment(value, fallback = "run") {
16
+ return String(value || fallback)
17
+ .trim()
18
+ .replace(/[^a-zA-Z0-9._-]+/g, "_")
19
+ .replace(/^_+|_+$/g, "")
20
+ .slice(0, 120) || fallback;
21
+ }
22
+
23
+ export function runLedgerId(prefix = "run") {
24
+ return `${safeSegment(prefix, "run")}-${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
25
+ }
26
+
27
+ export function runLedgerDir() {
28
+ return path.join(getAgentflowDataRoot(), "admin", "run-ledger");
29
+ }
30
+
31
+ export function runLedgerPath(timeMs = Date.now()) {
32
+ return path.join(runLedgerDir(), `${localDayKey(timeMs)}.jsonl`);
33
+ }
34
+
35
+ export function appendRunLedgerEvent(event = {}) {
36
+ try {
37
+ const item = {
38
+ version: 1,
39
+ type: String(event.type || ""),
40
+ kind: String(event.kind || ""),
41
+ runId: String(event.runId || runLedgerId()),
42
+ userId: String(event.userId || ""),
43
+ username: String(event.username || event.userId || ""),
44
+ flowId: String(event.flowId || ""),
45
+ flowSource: String(event.flowSource || "user"),
46
+ runNodeId: String(event.runNodeId || ""),
47
+ at: Number(event.at || event.startedAt || Date.now()),
48
+ endedAt: event.endedAt == null ? null : Number(event.endedAt),
49
+ durationMs: Math.max(0, Number(event.durationMs || 0)),
50
+ status: event.status ? String(event.status || "") : "",
51
+ };
52
+ if (!item.type || !item.kind || !item.runId || !item.flowId) return;
53
+ const filePath = runLedgerPath(item.at);
54
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
55
+ fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
56
+ } catch {
57
+ // Usage telemetry must never affect the run itself.
58
+ }
59
+ }
60
+
61
+ function readJsonlObjects(filePath) {
62
+ if (!fs.existsSync(filePath)) return [];
63
+ try {
64
+ const out = [];
65
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/);
66
+ for (const line of lines) {
67
+ if (!line.trim()) continue;
68
+ try {
69
+ const parsed = JSON.parse(line);
70
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) out.push(parsed);
71
+ } catch {
72
+ /* ignore malformed line */
73
+ }
74
+ }
75
+ return out;
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ export function readRunLedgerEvents(options = {}) {
82
+ const dir = runLedgerDir();
83
+ if (!fs.existsSync(dir)) return [];
84
+ try {
85
+ const sinceMs = Number(options?.sinceMs || 0);
86
+ const sinceKey = Number.isFinite(sinceMs) && sinceMs > 0 ? localDayKey(sinceMs) : "";
87
+ const files = fs.readdirSync(dir, { withFileTypes: true })
88
+ .filter((entry) => entry.isFile() && /^\d{4}-\d{2}-\d{2}\.jsonl$/.test(entry.name))
89
+ .filter((entry) => !sinceKey || entry.name.slice(0, 10) >= sinceKey)
90
+ .map((entry) => path.join(dir, entry.name))
91
+ .sort();
92
+ return files.flatMap((filePath) => readJsonlObjects(filePath));
93
+ } catch {
94
+ return [];
95
+ }
96
+ }